Skip to content

refactor(core): the reduced-motion mark carries a marker of its own, and the chat scroll, streaming text and swipe settle read it - #9763

Draft
cixzhang wants to merge 2 commits into
lane4/theme-always-markfrom
lane4/r8895-mark
Draft

cixzhang wants to merge 2 commits into
lane4/theme-always-markfrom
lane4/r8895-mark

Conversation

@cixzhang

@cixzhang cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Supersedes #8895, rebuilt on #9761 (Depends on #9761). Part of #8677's split. This is internal: no theme can set reducedMotion: 'always' yet, so there is no behavior change.

User impact

  • People using apps: none today. When 'always' ships, these changes make streaming text, the chat scroll's follow and a swipe row's settle reduce their motion for everyone under that theme, as they already do for a person whose Reduce Motion setting is on.
  • App builders: no API change.
  • Maintainers:
    • one marker class lets component styles key their Reduce Motion twins on the mark;
    • two guards with shrinking lists keep every Reduce Motion style and every script read of the setting moving onto the mark, family by family.

What changes

  • theme/reducedMotion.markers.stylex.ts: defines reducedMotionScope.
    • Theme puts it on the marked wrapper, and the root theme adds its class on <html> beside the mark.
    • A style keyed on stylex.when.ancestor('[data-astryx-reduced-motion]', reducedMotionScope) therefore reaches layers portaled to <body>.
    • It is a marker of its own because the default marker would also match every default-marker :hover rule under a theme.
  • motion/reducedMotion.ts: adds reducesMotion(element): the person's setting, or the mark on the element, an ancestor, or <html>.
  • hooks/useReducesMotion.ts: its hook form, for a hook that holds no element. It reads the setting live, or the theme tree through ThemeReducesMotionContext, where a nested theme never removes an ancestor's 'always'. It adds no effect.
  • Readers: useChatStreamScroll, useStreamingText and ItemSwipeLayer now read it instead of useMediaQuery('(prefers-reduced-motion: reduce)').
  • motion/reducedMotion.test.ts guards:
    • every Reduce Motion arm needs its twin on the mark, with pending files listed;
    • every script read of the setting needs the mark. A read through a constant that holds the query counts, for example BottomSheetPanel's REDUCED_MOTION_COARSE_QUERY.
    • The lists only shrink.
  • Pending lists, matched to current main:
    • ComplexSelector and Selector join the twin list;
    • the press release (utils/interactionOverlay.stylex.ts, utils/pressFeedback.ts) is pending. Its 'always' reader is a separate change.
  • theme/reducedMotionAlways.testing.ts: tests reach 'always' through a themeMotion mock, because no public theme can set it.
  • Changeset: [chore] Internal:.

No behavior change: resolveKnobs resolves every theme to 'system', so no theme marks its root and the new branches never run in a shipped app.

Earlier review findings

  • A query held in a constant: the setting-read guard resolves constants that hold the query, so BottomSheetPanel's read counts and that file stays on the pending list until its family converts.
  • Markdown streaming: the changeset no longer claims that Markdown's streamed chunks reach 'always'. Markdown's fadeIn stays on the twin list until its family pull request.

Evidence

  • theme/reducedMotionScope.test.tsx: the marker is in server markup and on <html>, and a mark-keyed style applies in the tree and in a layer portaled to <body>.
  • Reader tests, under an 'always' theme with the setting off:
    • streaming text snaps to the full text;
    • chat growth jumps;
    • a swipe settles at once.
    • Under a 'system' theme, all three keep their motion.
  • motion/reducedMotion.test.ts: both guards pass with the lists above.
  • Other checks: the theme, motion, Chat, Item and hooks suites pass. pnpm review:pre reports no findings. check:knowledge passes.

Credit

@vjeux's design and code from #8895, rebuilt on current main and #9761, with the attribute read through its owner module and the spec citations removed from comments. Co-authored-by trailer on the commit.

…and the chat scroll, streaming text and swipe settle read it

Internal, like the mark it builds on: no theme can set reducedMotion
'always' yet, so nothing changes for anyone.

- theme/reducedMotion.markers.stylex.ts defines reducedMotionScope. Theme
  puts it on the marked wrapper, and the root theme writes its class on
  <html> beside the mark, so a Reduce Motion style keyed on
  stylex.when.ancestor('[data-astryx-reduced-motion]', reducedMotionScope)
  reaches portaled layers.
- motion/reducedMotion.ts adds reducesMotion(element): the person's setting
  or the mark. hooks/useReducesMotion.ts is its hook form, for a hook that
  holds no element: the setting, followed live, or the theme tree through
  ThemeReducesMotionContext, where a nested theme never removes an
  ancestor's 'always'.
- useChatStreamScroll, useStreamingText and ItemSwipeLayer read them.
- motion/reducedMotion.test.ts guards the rest with lists that only shrink:
  every Reduce Motion arm needs its twin on the mark, and every script read
  of the setting needs the mark. A read through a constant that holds the
  query counts.
- Tests reach 'always' through a themeMotion mock
  (theme/reducedMotionAlways.testing.ts).

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 3, 2026 7:18pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 3, 2026
@astracat-bot

astracat-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟡 Reviewed (for maintainers only) Nov 3, 2026, 7:27 PM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested. The refactor changes no behavior today and fits the theme-motion spec, but the twin contract it introduces conflicts with the private-attribute rule.

  • The reduced-motion guard (packages/core/src/motion/reducedMotion.test.ts:50-51) and the examples in theme/reducedMotion.markers.stylex.ts and motion/reducedMotion.ts require each component to write stylex.when.ancestor('[data-astryx-reduced-motion]', reducedMotionScope) itself, which architecture:public-component-api INV23 forbids and @astryx/no-raw-private-attribute rejects in CI; move the twin selector into privateAttributes/reducedMotion.stylex.ts as ready-made styles and have the guard accept a spread of them.

Non-blocking: useReducesMotion reads only the theme context, so it misses a root 'always' inside useToast's detached fallback root, and a few new comments restate the hook (see the inline notes).

[Automated review]

Comment on lines +50 to +51
const TWIN =
/stylex\.when\.ancestor\(\s*'\[data-astryx-reduced-motion\]',\s*reducedMotionScope,?\s*\)/g;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking. This regex accepts a twin only when the component writes stylex.when.ancestor('[data-astryx-reduced-motion]', reducedMotionScope) itself. architecture:public-component-api INV23 keeps a private attribute's selectors in its owner module, and @astryx/no-raw-private-attribute reports this exact twin as an error under CI settings. Have privateAttributes/reducedMotion.stylex.ts export ready-made twin styles and let the guard accept a spread of them.

const prefersReducedMotion = useMediaQuery(
'(prefers-reduced-motion: reduce)',
);
const themeReducesMotion = use(ThemeReducesMotionContext);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this reads only the theme context, so a component in useToast's detached fallback root reads false even when the root theme marks <html>, which reducesMotion(element) and the CSS twins honour. Worth closing in the change that ships 'always'.

const leadingEntriesRef = useRef<HTMLDivElement | null>(null);
const trailingEntriesRef = useRef<HTMLDivElement | null>(null);
const isReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
// The setting, live, or a theme's reducedMotion: 'always'.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking (INV29): this restates useReducesMotion's JSDoc; delete it.

const prefersReducedMotion = useMediaQuery(
'(prefers-reduced-motion: reduce)',
);
// The setting, live, or a theme's reducedMotion: 'always'.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking (INV29): this restates useReducesMotion's JSDoc; delete it.

Comment on lines +175 to +177
// Reduced motion skips the progressive reveal and shows the full text: the
// person's setting, followed live so a change mid-stream takes effect, or a
// theme's reducedMotion: 'always'.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking (INV29): one 27-word sentence with two topics. Suggested: "Reduced motion skips the progressive reveal and shows the full text. A change mid-stream takes effect at once."

Comment on lines +301 to +306
// 'always' marks the theme's root with the attribute, its custom property
// and the reducedMotionScope marker. Script readers see it (reducesMotion,
// useReducesMotion), and each component Reduce Motion style keys a twin on
// it; motion/reducedMotion.test.ts lists the styles that still lack theirs.
// The mark only adds: a nested theme keeps an ancestor's.
// resolveMotionKnobs resolves no public theme to 'always'.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking (INV29): the first sentences narrate the readers and point at a test's list. Suggested: "The mark only adds: a nested theme keeps an ancestor's. No public theme resolves to 'always' yet."

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested. The refactor changes no behavior today and fits the theme-motion spec, but the twin contract it sets up conflicts with the private-attribute rule.

  • The reduced-motion guard (packages/core/src/motion/reducedMotion.test.ts:50-51) and the examples in theme/reducedMotion.markers.stylex.ts and motion/reducedMotion.ts have each component write stylex.when.ancestor('[data-astryx-reduced-motion]', reducedMotionScope) itself, which architecture:public-component-api INV23 forbids and @astryx/no-raw-private-attribute rejects in CI; move the twin selector into privateAttributes/reducedMotion.stylex.ts as ready-made styles and have the guard accept a spread of them.

Non-blocking: useReducesMotion reads only the theme context, so it misses a root 'always' in a separate React root such as useToast's fallback viewport, which matters once 'always' ships. A few new comments restate the hook or narrate the code (Item/ItemSwipeLayer.tsx:208, Chat/useChatStreamScroll.ts:260, hooks/useStreamingText.ts:175-177, theme/Theme.tsx:301-306).

[Automated review]

@github-actions
github-actions Bot deleted the branch lane4/theme-always-mark November 4, 2026 06:04

This branch was successfully deployed

1 active deployment
Preview — f725cf66 Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant